use std::io::Write;

use crate::{
    error::*,
    console::Console,
    syscall::{self, OpenFlags},
};

struct ProcessArguments {
    input_path: String,
    output_path: String,
    args: Vec<String>,
}

impl ProcessArguments {
    fn new(command: &str) -> Self {
        let args: Vec<_> = command.split(' ').collect();
        let mut args: Vec<String> = args
            .iter()
            .filter(|&arg| !arg.is_empty())
            .map(|&arg| arg.to_string())
            .collect();
        
        // Redirect input
        // command arg0 arg1 > output_file < input_file
        let input_path = if let Some((idx, _)) = args
            .iter()
            .enumerate()
            .find(|(_, arg)| arg.as_str() == "<")
        {
            let input_path = args[idx + 1].clone();
            args.drain(idx..=idx + 1);
            input_path
        } else {
            String::new()
        };

        // Redirect output
        // command arg0 arg1 > output_file
        let output_path = if let Some((idx, _)) = args
            .iter()
            .enumerate()
            .find(|(_, arg)| arg.as_str() == ">")
        {
            let output_path = args[idx + 1].clone();
            args.drain(idx..=idx + 1);
            output_path
        } else {
            String::new()
        };

        Self {
            input_path, 
            output_path,
            args
        }
    }
}

pub struct Shell {
    console: Console,
}

impl Shell {
    pub fn new() -> Self {
        Shell {
            console: Console::new(),
        }
    }

    pub fn run(&mut self) -> Result<()> {
        while !self.console.exit() {
            let input = self.read_input()?;
            self.execute(&input)?;
        }
        Ok(())
    }
}

impl Shell {
    fn read_input(&mut self) -> Result<String> {
        self.console.read_line()
    }

    fn execute(&mut self, input: &str) -> Result<()> {
        let input = input.trim();
        std::io::stdout().flush()?;
        if input.is_empty() {
            return Ok(());
        }

        let commands: Vec<_> = input.split('|').collect();
        let process_args_list: Vec<ProcessArguments> = commands
            .iter()
            .map(|&cmd| ProcessArguments::new(cmd))
            .collect();

        let mut valid = true;
        for (idx, process_args) in process_args_list.iter().enumerate() {
            // First cmd can't have output
            if idx == 0 {
                if !process_args.output_path.is_empty()  {
                    valid = false;
                }
            // Last cmd can't have input
            } else if idx == process_args_list.len() - 1 {
                if !process_args.input_path.is_empty() {
                    valid = false;
                }
            }
            // Middle cmds can't have input or ouput 
            else {
                if !process_args.output_path.is_empty() || !process_args.input_path.is_empty() {
                    valid = false;
                }
            }
        }

        if process_args_list.len() == 1 {
            valid = true;
        }

        if !valid {
            self.console.println("Invalid command: Inputs/Outputs cannot be correctly binded!")?;
            return Ok(());
        }

        // Create pipes
        let pipes_fd: Vec<(i32, i32)> = 
        if !process_args_list.is_empty() {
            (0..process_args_list.len() - 1)
                .into_iter()
                .map(|_| syscall::pipe().1)
                .collect()
        } else {
            Vec::new()
        };

        let mut childrne = Vec::new();
        for (idx, process_args) in process_args_list.iter().enumerate() {
            let pid = syscall::fork();            
            if pid == 0 {
                let input_path = process_args.input_path.as_str();
                let output_path = process_args.output_path.as_str();
                let args: Vec<_> = process_args.args.iter().map(|s| s.as_str()).collect();

                // Redirect input
                if !input_path.is_empty() {
                    let input_fd = syscall::open(input_path, OpenFlags::RDONLY);
                    syscall::close(0);
                    assert_eq!(syscall::dup(input_fd), 0);
                    syscall::close(input_fd);
                }
                
                // Redirect output
                if !output_path.is_empty() {
                    let output_fd = syscall::open(output_path, OpenFlags::CREATE | OpenFlags::WRONLY);
                    syscall::close(1);
                    assert_eq!(syscall::dup(output_fd), 1);
                    syscall::close(output_fd);
                }

                // Receive input from previous process
                if idx > 0 {
                    syscall::close(0);
                    let read_fd = pipes_fd[idx - 1].0;
                    assert_eq!(syscall::dup(read_fd), 0);
                }

                // Send ouptut to the next process
                if idx < process_args_list.len() - 1 {
                    syscall::close(1);
                    let write_fd = pipes_fd[idx].1;
                    assert_eq!(syscall::dup(write_fd), 1);
                }
                
                // Close all pipe ends inherited from the parent process
                for pipe_fd in pipes_fd.iter() {
                    syscall::close(pipe_fd.0);
                    syscall::close(pipe_fd.1);
                }

                // Execute new application
                syscall::execvp(&args[0],&args[1..]);
            } else {
                childrne.push(pid);
            }
        }

        for pipe_fd in pipes_fd.iter() {
            syscall::close(pipe_fd.0);
            syscall::close(pipe_fd.1);
        }

        for pid in childrne.into_iter() {
            syscall::waitpid(pid);
        } 

        Ok(())
    }

}